/*
 *  Copyright (C) 2010 Broadcom
 *  Copyright (C) 2015 Noralf Trønnes
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 */

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/ioctl.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <soc/bcm2835/raspberrypi-firmware.h>

#define MBOX_CHAN_PROPERTY 8

#define VCIO_IOC_MAGIC 100
#define IOCTL_MBOX_PROPERTY _IOWR(VCIO_IOC_MAGIC, 0, char *)

static struct {
	dev_t devt;
	struct cdev cdev;
	struct class *class;
	struct rpi_firmware *fw;
} vcio;

static int vcio_user_property_list(void *user)
{
	u32 *buf, size;
	int ret;

	/* The first 32-bit is the size of the buffer */
	if (copy_from_user(&size, user, sizeof(size)))
		return -EFAULT;

	buf = kmalloc(size, GFP_KERNEL);
	if (!buf)
		return -ENOMEM;

	if (copy_from_user(buf, user, size)) {
		kfree(buf);
		return -EFAULT;
	}

	/* Strip off protocol encapsulation */
	ret = rpi_firmware_property_list(vcio.fw, &buf[2], size - 12);
	if (ret) {
		kfree(buf);
		return ret;
	}

	buf[1] = RPI_FIRMWARE_STATUS_SUCCESS;
	if (copy_to_user(user, buf, size))
		ret = -EFAULT;

	kfree(buf);

	return ret;
}

static int vcio_device_open(struct inode *inode, struct file *file)
{
	try_module_get(THIS_MODULE);

	return 0;
}

static int vcio_device_release(struct inode *inode, struct file *file)
{
	module_put(THIS_MODULE);

	return 0;
}

static long vcio_device_ioctl(struct file *file, unsigned int ioctl_num,
			      unsigned long ioctl_param)
{
	switch (ioctl_num) {
	case IOCTL_MBOX_PROPERTY:
		return vcio_user_property_list((void *)ioctl_param);
	default:
		pr_err("unknown ioctl: %d\n", ioctl_num);
		return -EINVAL;
	}
}

const struct file_operations vcio_fops = {
	.unlocked_ioctl = vcio_device_ioctl,
	.open = vcio_device_open,
	.release = vcio_device_release,
};

static int __init vcio_init(void)
{
	struct device_node *np;
	static struct device *dev;
	int ret;

	np = of_find_compatible_node(NULL, NULL,
				     "raspberrypi,bcm2835-firmware");
/* Uncomment this when we only boot with Device Tree
	if (!of_device_is_available(np))
		return -ENODEV;
*/
	vcio.fw = rpi_firmware_get(np);
	if (!vcio.fw)
		return -ENODEV;

	ret = alloc_chrdev_region(&vcio.devt, 0, 1, "vcio");
	if (ret) {
		pr_err("failed to allocate device number\n");
		return ret;
	}

	cdev_init(&vcio.cdev, &vcio_fops);
	vcio.cdev.owner = THIS_MODULE;
	ret = cdev_add(&vcio.cdev, vcio.devt, 1);
	if (ret) {
		pr_err("failed to register device\n");
		goto err_unregister_chardev;
	}

	/*
	 * Create sysfs entries
	 * 'bcm2708_vcio' is used for backwards compatibility so we don't break
	 * userspace. Raspian has a udev rule that changes the permissions.
	 */
	vcio.class = class_create(THIS_MODULE, "bcm2708_vcio");
	if (IS_ERR(vcio.class)) {
		ret = PTR_ERR(vcio.class);
		pr_err("failed to create class\n");
		goto err_cdev_del;
	}

	dev = device_create(vcio.class, NULL, vcio.devt, NULL, "vcio");
	if (IS_ERR(dev)) {
		ret = PTR_ERR(dev);
		pr_err("failed to create device\n");
		goto err_class_destroy;
	}

	return 0;

err_class_destroy:
	class_destroy(vcio.class);
err_cdev_del:
	cdev_del(&vcio.cdev);
err_unregister_chardev:
	unregister_chrdev_region(vcio.devt, 1);

	return ret;
}
module_init(vcio_init);

static void __exit vcio_exit(void)
{
	device_destroy(vcio.class, vcio.devt);
	class_destroy(vcio.class);
	cdev_del(&vcio.cdev);
	unregister_chrdev_region(vcio.devt, 1);
}
module_exit(vcio_exit);

MODULE_AUTHOR("Gray Girling");
MODULE_AUTHOR("Noralf Trønnes");
MODULE_DESCRIPTION("Mailbox userspace access");
MODULE_LICENSE("GPL");
